iT邦幫忙

2025 iThome 鐵人賽

DAY 13
0
Software Development

Polars熊霸天下系列 第 13

[Day13] - Datatype:Temporal

  • 分享至 

  • xImage
  •  

今天我們來了解與時間有關的型別(在Polars中習慣稱呼其為temporal型別)及操作。

Polars共有四種temporal型別:

  • pl.Date:日期型別,像是2025-09-01,內部儲存型態為pl.Int32,代表自UNIX epoch(即UTC1970年1月1日0時0分0秒)以來的天數。
  • pl.Datetime:日期及時間型別,像是2025-09-01 07:00:00,內部儲存型態為pl.Int64,代表自UNIX epoch以來的秒數,可以選擇三種秒數單位,分別為usnsms,預設值為us(microseconds)。
  • pl.Duration:兩種時間型別的差值,類似於Python的timedelta
  • pl.Time:代表自午夜以來的秒數,單位為nanoseconds。

本日大綱如下:

  1. 本日引入模組及準備工作
  2. 基本操作
  3. pl.DataFrame.group_by_dynamic()
  4. pl.DataFrame.upsample()
  5. codepanda

0. 本日引入模組及準備工作

使用多個生成時間序列的函數,來建立df dataframe。

pl.date_range()為例,其共有五個參數:

  • start=:起始日期。
  • end=:結束日期。
  • interval=:時間間隔。
  • closed=:是否包括起始與結束日期。共有both(預設)、leftright三種。
  • eager=:預設為False,代表不立即生成,而是返回expr。如果設定為True,則會立即生成,並返回series。
from datetime import date, datetime, time, timedelta

import pandas as pd
import polars as pl

df = pl.DataFrame(
    {
        "date": pl.date_range(
            date(2025, 1, 1), date(2025, 6, 1), interval="1mo", eager=True
        ),
        "date_str": [
            "2025-07-05",
            "2025-08-05",
            "2025-09-10",
            "2025-10-10",
            "2025-11-20",
            "2025-12-20",
        ],
        "datetime": pl.datetime_range(
            datetime(2025, 1, 1),
            datetime(2025, 1, 2),
            interval="4h",
            closed="left",
            eager=True,
        ),
        "datetime_utc": pl.datetime_range(
            datetime(2025, 1, 1),
            datetime(2025, 6, 1),
            interval="1mo",
            eager=True,
            time_zone="UTC",
        ),
        "time": pl.time_range(
            time(13, 0, 0),
            time(13, 25, 0),
            interval=timedelta(minutes=5),
            eager=True,
        ),
    }
)
shape: (6, 5)
┌────────────┬────────────┬─────────────────────┬─────────────────────────┬
│ date       ┆ date_str   ┆ datetime            ┆ datetime_utc            ┆
│ ---        ┆ ---        ┆ ---                 ┆ ---                     ┆
│ date       ┆ str        ┆ datetime[μs]        ┆ datetime[μs, UTC]       ┆
╞════════════╪════════════╪═════════════════════╪═════════════════════════╪
│ 2025-01-01 ┆ 2025-07-05 ┆ 2025-01-01 00:00:00 ┆ 2025-01-01 00:00:00 UTC ┆
│ 2025-02-01 ┆ 2025-08-05 ┆ 2025-01-01 04:00:00 ┆ 2025-02-01 00:00:00 UTC ┆
│ 2025-03-01 ┆ 2025-09-10 ┆ 2025-01-01 08:00:00 ┆ 2025-03-01 00:00:00 UTC ┆
│ 2025-04-01 ┆ 2025-10-10 ┆ 2025-01-01 12:00:00 ┆ 2025-04-01 00:00:00 UTC ┆
│ 2025-05-01 ┆ 2025-11-20 ┆ 2025-01-01 16:00:00 ┆ 2025-05-01 00:00:00 UTC ┆
│ 2025-06-01 ┆ 2025-12-20 ┆ 2025-01-01 20:00:00 ┆ 2025-06-01 00:00:00 UTC ┆
└────────────┴────────────┴─────────────────────┴─────────────────────────┴
┬──────────┐
┆ time     │
┆ ---      │
┆ time     │
╪══════════╡
┆ 13:00:00 │
┆ 13:05:00 │
┆ 13:10:00 │
┆ 13:15:00 │
┆ 13:20:00 │
┆ 13:25:00 │
┴──────────┘

1. 基本操作

以下我們將透過幾段程式碼,認識temporal的各種型別及dt命名空間提供的各種功能。

pl.Date轉換為pl.String

下面這段程式碼展示了:

  • 使用pl.Expr.dt.strftime()(註1)將「"date"」列轉換為pl.String型別。
  • 或是使用pl.Expr.dt.month()自「"date"」列提取出月份。請注意當提取temporal型別中的時間資訊時,需要加上(),也就是需要使用pl.Expr.dt.month()。習慣使用Pandas的朋友,常常會寫成pl.Expr.dt.month
  • 利用pl.Expr.cast將「"date"」列轉換為pl.Int32型別,可以得到其距離UNIX epoch的天數。
  • 利用pl.Expr.cast將「"datetime"」列轉換為pl.Int64型別,可以得到其距離UNIX epoch的秒數(單位為microseconds)。
(
    df.select(
        pl.col("date"),
        pl.col("date").dt.strftime("%Y/%m/%d").alias("strftime"),
        pl.col("date").dt.month().alias("month"),  # not `dt.month`
        pl.col("date").cast(pl.Int32).alias("date_in_days"),
        pl.col("datetime").cast(pl.Int64).alias("datetime_in_microsecs"),
    )
)
shape: (6, 5)
┌────────────┬────────────┬───────┬──────────────┬───────────────────────┐
│ date       ┆ strftime   ┆ month ┆ date_in_days ┆ datetime_in_microsecs │
│ ---        ┆ ---        ┆ ---   ┆ ---          ┆ ---                   │
│ date       ┆ str        ┆ i8    ┆ i32          ┆ i64                   │
╞════════════╪════════════╪═══════╪══════════════╪═══════════════════════╡
│ 2025-01-01 ┆ 2025/01/01 ┆ 1     ┆ 20089        ┆ 1735689600000000      │
│ 2025-02-01 ┆ 2025/02/01 ┆ 2     ┆ 20120        ┆ 1735704000000000      │
│ 2025-03-01 ┆ 2025/03/01 ┆ 3     ┆ 20148        ┆ 1735718400000000      │
│ 2025-04-01 ┆ 2025/04/01 ┆ 4     ┆ 20179        ┆ 1735732800000000      │
│ 2025-05-01 ┆ 2025/05/01 ┆ 5     ┆ 20209        ┆ 1735747200000000      │
│ 2025-06-01 ┆ 2025/06/01 ┆ 6     ┆ 20240        ┆ 1735761600000000      │
└────────────┴────────────┴───────┴──────────────┴───────────────────────┘

pl.String轉換為pl.Date

下面這段程式碼展示了:

(
    df.with_columns(
        pl.col("date_str").str.to_date().alias("to_date"),
    )
    .with_columns(
        pl.col("date").sub(pl.col("to_date")).alias("duration"),
    )
    .select(
        pl.col("date_str", "to_date", "duration"),
        pl.col("duration").dt.total_days().alias("duration_days"),
        pl.col("duration").dt.total_hours().alias("duration_hours"),
    )
)
shape: (6, 5)
┌────────────┬────────────┬──────────────┬───────────────┬────────────────┐
│ date_str   ┆ to_date    ┆ duration     ┆ duration_days ┆ duration_hours │
│ ---        ┆ ---        ┆ ---          ┆ ---           ┆ ---            │
│ str        ┆ date       ┆ duration[ms] ┆ i64           ┆ i64            │
╞════════════╪════════════╪══════════════╪═══════════════╪════════════════╡
│ 2025-07-05 ┆ 2025-07-05 ┆ -185d        ┆ -185          ┆ -4440          │
│ 2025-08-05 ┆ 2025-08-05 ┆ -185d        ┆ -185          ┆ -4440          │
│ 2025-09-10 ┆ 2025-09-10 ┆ -193d        ┆ -193          ┆ -4632          │
│ 2025-10-10 ┆ 2025-10-10 ┆ -192d        ┆ -192          ┆ -4608          │
│ 2025-11-20 ┆ 2025-11-20 ┆ -203d        ┆ -203          ┆ -4872          │
│ 2025-12-20 ┆ 2025-12-20 ┆ -202d        ┆ -202          ┆ -4848          │
└────────────┴────────────┴──────────────┴───────────────┴────────────────┘

合併pl.Date/DateTimepl.Timepl.Datetime

下面這段程式碼展示了:

  • 使用Pl.Expr.dt.combine()來將「"date"」列與「"time"」列合併。
  • 利用pl.Expr.cast將「"time"」列轉換為pl.Int64型別,可以得到其距離午夜的秒數(單位為nanoseconds)。
(
    df.select(
        "date",
        "time",
        pl.col("date")
        .dt.combine(pl.col("time"))
        .alias("combined_datetime"),
        pl.col("time").cast(pl.Int64).alias("time_in_nanosecs"),
    )
)
shape: (6, 4)
┌────────────┬──────────┬─────────────────────┬──────────────────┐
│ date       ┆ time     ┆ combined_datetime   ┆ time_in_nanosecs │
│ ---        ┆ ---      ┆ ---                 ┆ ---              │
│ date       ┆ time     ┆ datetime[μs]        ┆ i64              │
╞════════════╪══════════╪═════════════════════╪══════════════════╡
│ 2025-01-01 ┆ 13:00:00 ┆ 2025-01-01 13:00:00 ┆ 46800000000000   │
│ 2025-02-01 ┆ 13:05:00 ┆ 2025-02-01 13:05:00 ┆ 47100000000000   │
│ 2025-03-01 ┆ 13:10:00 ┆ 2025-03-01 13:10:00 ┆ 47400000000000   │
│ 2025-04-01 ┆ 13:15:00 ┆ 2025-04-01 13:15:00 ┆ 47700000000000   │
│ 2025-05-01 ┆ 13:20:00 ┆ 2025-05-01 13:20:00 ┆ 48000000000000   │
│ 2025-06-01 ┆ 13:25:00 ┆ 2025-06-01 13:25:00 ┆ 48300000000000   │
└────────────┴──────────┴─────────────────────┴──────────────────┘

時區

Python的zoneinfo.available_timezones()可以列出Polars支援的時區:

import zoneinfo

print(zoneinfo.available_timezones())

下面這段程式碼展示了:

(
    df.select(
        pl.col("datetime_utc"),
        pl.col("datetime_utc")
        .dt.convert_time_zone("Asia/Taipei")
        .alias("convert_tz_tpe"),
        pl.col("datetime_utc")
        .dt.replace_time_zone("Asia/Taipei")
        .alias("replace_tz_tpe"),
    )
)
shape: (6, 3)
┌─────────────────────────┬───────────────────────────┬
│ datetime_utc            ┆ convert_tz_tpe            ┆
│ ---                     ┆ ---                       ┆
│ datetime[μs, UTC]       ┆ datetime[μs, Asia/Taipei] ┆
╞═════════════════════════╪═══════════════════════════╪
│ 2025-01-01 00:00:00 UTC ┆ 2025-01-01 08:00:00 CST   ┆
│ 2025-02-01 00:00:00 UTC ┆ 2025-02-01 08:00:00 CST   ┆
│ 2025-03-01 00:00:00 UTC ┆ 2025-03-01 08:00:00 CST   ┆
│ 2025-04-01 00:00:00 UTC ┆ 2025-04-01 08:00:00 CST   ┆
│ 2025-05-01 00:00:00 UTC ┆ 2025-05-01 08:00:00 CST   ┆
│ 2025-06-01 00:00:00 UTC ┆ 2025-06-01 08:00:00 CST   ┆
└─────────────────────────┴───────────────────────────┴
┬───────────────────────────┐
┆ replace_tz_tpe            │
┆ ---                       │
┆ datetime[μs, Asia/Taipei] │
╪═══════════════════════════╡
┆ 2025-01-01 00:00:00 CST   │
┆ 2025-02-01 00:00:00 CST   │
┆ 2025-03-01 00:00:00 CST   │
┆ 2025-04-01 00:00:00 CST   │
┆ 2025-05-01 00:00:00 CST   │
┆ 2025-06-01 00:00:00 CST   │
┴───────────────────────────

請留意,Pl.Expr.dt.convert_time_zone()是實際上對pl.Datetime中的時區資訊進行轉換,而Pl.Expr.dt.replace_time_zone()則是將pl.Datetime中的時區資訊取代(也可以想成指定)為另一個時區。

依照這個邏輯,我們既然可以使用Pl.Expr.dt.replace_time_zone()指定時區,那麼應該也可以將時區資訊自pl.Datetime刪去。實際上,如果將None傳入Pl.Expr.dt.replace_time_zone()的確可以達成這樣的效果:

(
    df.select(
        pl.col("datetime_utc"),
        pl.col("datetime_utc")
        .dt.replace_time_zone(None)
        .alias("no_tz"),
    )
)
shape: (6, 2)
┌─────────────────────────┬─────────────────────┐
│ datetime_utc            ┆ no_tz               │
│ ---                     ┆ ---                 │
│ datetime[μs, UTC]       ┆ datetime[μs]        │
╞═════════════════════════╪═════════════════════╡
│ 2025-01-01 00:00:00 UTC ┆ 2025-01-01 00:00:00 │
│ 2025-02-01 00:00:00 UTC ┆ 2025-02-01 00:00:00 │
│ 2025-03-01 00:00:00 UTC ┆ 2025-03-01 00:00:00 │
│ 2025-04-01 00:00:00 UTC ┆ 2025-04-01 00:00:00 │
│ 2025-05-01 00:00:00 UTC ┆ 2025-05-01 00:00:00 │
│ 2025-06-01 00:00:00 UTC ┆ 2025-06-01 00:00:00 │
└─────────────────────────┴─────────────────────┘

2. pl.DataFrame.group_by_dynamic()

pl.DataFrame.group_by_dynamic()可以讓我們依據想要的時間間隔進行分組聚合。例如,我們可以將「"date"」列依照兩個月一次的頻率分組(註2),將「"time"」列中的pl.Time收集為pl.List

df.group_by_dynamic("date", every="2mo").agg(pl.col("time"))
shape: (3, 2)
┌────────────┬──────────────────────┐
│ date       ┆ time                 │
│ ---        ┆ ---                  │
│ date       ┆ list[time]           │
╞════════════╪══════════════════════╡
│ 2025-01-01 ┆ [13:00:00, 13:05:00] │
│ 2025-03-01 ┆ [13:10:00, 13:15:00] │
│ 2025-05-01 ┆ [13:20:00, 13:25:00] │
└────────────┴──────────────────────┘

除了指定時間間隔的every=參數外,另一個常用的period=參數可以指定聚合時所使用的時間段。例如,我們可以將「"date"」列,依照兩個月一次的頻率分組,並以三個月做聚合計算,收集「"time"」列中的pl.Time

(
    df.group_by_dynamic("date", every="2mo", period="3mo").agg(
        pl.col("time")
    )
)
shape: (3, 2)
┌────────────┬────────────────────────────────┐
│ date       ┆ time                           │
│ ---        ┆ ---                            │
│ date       ┆ list[time]                     │
╞════════════╪════════════════════════════════╡
│ 2025-01-01 ┆ [13:00:00, 13:05:00, 13:10:00] │
│ 2025-03-01 ┆ [13:10:00, 13:15:00, 13:20:00] │
│ 2025-05-01 ┆ [13:20:00, 13:25:00]           │
└────────────┴────────────────────────────────┘

事實上,在不指定period=時,其值會與every=相等,也就是分組與聚合使用一樣的時間間隔。

此外,df.group_by_dynamic()有一個常被大家忽略的好用參數group_by=,可以幫助我們在針對temporal型別做分組時,同時對其它列或expr也進行分組。例如我們可以同時對「"date"」列及pl.col("date_str").str.slice(-2)進行分組聚合:

(
    df.group_by_dynamic(
        "date", every="2mo", group_by=pl.col("date_str").str.slice(-2)
    ).agg(pl.col("time"))
)
shape: (3, 3)
┌──────────┬────────────┬──────────────────────┐
│ date_str ┆ date       ┆ time                 │
│ ---      ┆ ---        ┆ ---                  │
│ str      ┆ date       ┆ list[time]           │
╞══════════╪════════════╪══════════════════════╡
│ 05       ┆ 2025-01-01 ┆ [13:00:00, 13:05:00] │
│ 10       ┆ 2025-03-01 ┆ [13:10:00, 13:15:00] │
│ 20       ┆ 2025-05-01 ┆ [13:20:00, 13:25:00] │
└──────────┴────────────┴──────────────────────┘

3. pl.DataFrame.upsample()

pl.DataFrame.upsample()可以幫助我們提高時間間隔取樣頻率。例如,我們可以將「"date"」列由四小時間隔提高為兩小時:

with pl.Config(tbl_rows=20):
    print(
        df.select("datetime", "date_str").upsample("datetime", every="2h")
    )
shape: (11, 2)
┌─────────────────────┬────────────┐
│ datetime            ┆ date_str   │
│ ---                 ┆ ---        │
│ datetime[μs]        ┆ str        │
╞═════════════════════╪════════════╡
│ 2025-01-01 00:00:00 ┆ 2025-07-05 │
│ 2025-01-01 02:00:00 ┆ null       │
│ 2025-01-01 04:00:00 ┆ 2025-08-05 │
│ 2025-01-01 06:00:00 ┆ null       │
│ 2025-01-01 08:00:00 ┆ 2025-09-10 │
│ 2025-01-01 10:00:00 ┆ null       │
│ 2025-01-01 12:00:00 ┆ 2025-10-10 │
│ 2025-01-01 14:00:00 ┆ null       │
│ 2025-01-01 16:00:00 ┆ 2025-11-20 │
│ 2025-01-01 18:00:00 ┆ null       │
│ 2025-01-01 20:00:00 ┆ 2025-12-20 │
└─────────────────────┴────────────┘

當然,Polars沒辦法無中生有,提高取樣頻率後的值預設為null,需要由使用者填入適當的值,例如使用pl.Expr.fill_null(),以「"forward"」做為strategy=參數,填補缺失值:

with pl.Config(tbl_rows=20):
    print(
        df.select("datetime", "date_str")
        .upsample("datetime", every="2h")
        .fill_null(strategy="forward")
    )
shape: (11, 2)
┌─────────────────────┬────────────┐
│ datetime            ┆ date_str   │
│ ---                 ┆ ---        │
│ datetime[μs]        ┆ str        │
╞═════════════════════╪════════════╡
│ 2025-01-01 00:00:00 ┆ 2025-07-05 │
│ 2025-01-01 02:00:00 ┆ 2025-07-05 │
│ 2025-01-01 04:00:00 ┆ 2025-08-05 │
│ 2025-01-01 06:00:00 ┆ 2025-08-05 │
│ 2025-01-01 08:00:00 ┆ 2025-09-10 │
│ 2025-01-01 10:00:00 ┆ 2025-09-10 │
│ 2025-01-01 12:00:00 ┆ 2025-10-10 │
│ 2025-01-01 14:00:00 ┆ 2025-10-10 │
│ 2025-01-01 16:00:00 ┆ 2025-11-20 │
│ 2025-01-01 18:00:00 ┆ 2025-11-20 │
│ 2025-01-01 20:00:00 ┆ 2025-12-20 │
└─────────────────────┴────────────┘

4. codepanda

相比於Polars,Pandas在時間相關型別提供了好用的offset alias

舉例來說,假如我們想知道此次鐵人賽報名時間的每個星期三,分別是開始報名後的第幾天,可以使用W-WED做為pd.DataFrame.resample()的規則。其中W代表以每週為resample目標,而WED則代表以星期三為分界。

idx = pd.date_range("2025-08-01", "2025-09-15")
df_pd = pd.DataFrame({"n": range(1, idx.size + 1)}).set_index(idx)
             n
2025-08-01   1
2025-08-02   2
2025-08-03   3
2025-08-04   4
...
2025-09-12  43
2025-09-13  44
2025-09-14  45
2025-09-15  46
df_pd.resample("W-WED").max()
             n
2025-08-06   6
2025-08-13  13
2025-08-20  20
2025-08-27  27
2025-09-03  34
2025-09-10  41
2025-09-17  46

備註

註1:pl.Expr.dt.strftime()會於底層呼叫pl.Expr.dt.to_string()。前者命名比較偏Python風格,而後者則比較偏Rust。

註2:關於時間間隔,Polars提供了一系列的寫法:

shape: (12, 2)
┌──────┬────────────────────┐
│ rule ┆ representation     │
│ ---  ┆ ---                │
│ str  ┆ str                │
╞══════╪════════════════════╡
│ 1ns  ┆ 1 nanosecond       │
│ 1us  ┆ 1 microsecond      │
│ 1ms  ┆ 1 millisecond      │
│ 1s   ┆ 1 second           │
│ 1m   ┆ 1 minute           │
│ 1h   ┆ 1 hour             │
│ 1d   ┆ 1 calendar day     │
│ 1w   ┆ 1 calendar week    │
│ 1mo  ┆ 1 calendar month   │
│ 1q   ┆ 1 calendar quarter │
│ 1y   ┆ 1 calendar year    │
│ 1i   ┆ 1 index count      │
└──────┴────────────────────┘

Code

本日程式碼傳送門


上一篇
[Day12] - Context:pl.DataFrame.group_by()
下一篇
[Day14] - Datatype:pl.Enum與pl.Categorical
系列文
Polars熊霸天下14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言